use moss::syn_parse::SynParse;
use proc_macro2::Span;
use proc_macro2::TokenStream;
use quote::quote;
use quote::ToTokens;
use syn::parse::ParseStream;
use syn::Ident;
use syn::ImplItem;
use syn::Item;
use syn::ItemImpl;
use syn::ItemTrait;
use syn::LitStr;
use syn::Path;
use syn::Result;
use syn::Signature;
use syn::TraitItem;

use crate::attrs::attrs_tokens_string;
use crate::attrs::AttrType;

pub fn expand_impl(
    class_token: &Option<TokenStream>,
    item_impl: &mut ItemImpl,
    callable: bool,
) -> Result<TokenStream> {
    let mut expanded = quote!();
    let self_ty = &item_impl.self_ty;
    let ty_token = Some(quote!(#self_ty));
    let mut trait_name: Option<String> = None;
    if let Some(trait_ty) = &item_impl.trait_ {
        trait_name = Some(trait_ty.1.to_token_stream().to_string());
    }
    for item_fn in &item_impl.items {
        if let ImplItem::Fn(item_fn) = item_fn {
            let fn_token = generate_submit_item(
                &item_fn.attrs,
                &item_fn.sig,
                class_token,
                &ty_token,
                &trait_name,
                callable,
            )?;
            expanded = quote! {
                #expanded

                #fn_token
            }
        }
    }
    Ok(expanded)
}

pub fn expand_trait(
    class_token: &Option<TokenStream>,
    item_impl: &mut ItemTrait,
) -> Result<TokenStream> {
    let mut expanded = quote!();
    let trait_name: Option<String> = Some(item_impl.ident.to_string());
    for item_fn in &item_impl.items {
        if let TraitItem::Fn(item_fn) = item_fn {
            let fn_token = generate_submit_item(
                &item_fn.attrs,
                &item_fn.sig,
                class_token,
                &None,
                &trait_name,
                false,
            )?;
            expanded = quote! {
                #expanded

                #fn_token
            }
        }
    }
    Ok(expanded)
}

pub fn generate_submit_item(
    attrs: &[syn::Attribute],
    sig: &Signature,
    class_token: &Option<TokenStream>,
    ty_token: &Option<TokenStream>,
    trait_name: &Option<String>,
    callable: bool,
) -> Result<TokenStream> {
    let mut trait_name_token = quote!(None);
    if let Some(trait_name) = trait_name {
        trait_name_token = quote!(Some(#trait_name.into()));
    }

    let attrs = attrs_tokens_string(attrs, AttrType::Other);
    let docs_token = attrs.docs;
    let attrs_token = attrs.attrs;
    let ident = &sig.ident;
    let ident_name = ident.to_string();

    let mut function_token = quote!();
    let mut inputs_token = quote!();

    let is_async = sig.asyncness.is_some();
    for input in &sig.inputs {
        match input {
            syn::FnArg::Receiver(_) => {
                if let Some(ty_token) = ty_token {
                    inputs_token = quote! {
                        #inputs_token
                        glass::Input {
                            name: "self".into(),
                            ty: <#ty_token as glass::Reflect>::type_info,
                        },
                    };
                    function_token = quote! {
                        #function_token
                        #ty_token,
                    };
                }
            }
            syn::FnArg::Typed(pat_type) => {
                let name = pat_type.pat.to_token_stream().to_string();
                let ty = &pat_type.ty;
                inputs_token = quote! {
                    #inputs_token
                    glass::Input {
                        name: #name.into(),
                        ty: <#ty as glass::Reflect>::type_info,
                    },
                };
                function_token = quote! {
                    #function_token
                    #ty,
                };
            }
        }
    }
    let output_token = match &sig.output {
        syn::ReturnType::Default => {
            function_token = quote! {
                #function_token
                (),
            };
            quote!(None)
        }
        syn::ReturnType::Type(_rarrow, ty) => {
            let output_ty_str = ty.to_token_stream().to_string();
            if output_ty_str.eq("Self") {
                if ty_token.is_some() {
                    function_token = quote! {
                        #function_token
                        #ty_token,
                    };
                    quote!(Some(<#ty_token as glass::Reflect>::type_info))
                } else {
                    quote!(None)
                }
            } else {
                function_token = quote! {
                    #function_token
                    #ty,
                };
                quote!(Some(<#ty as glass::Reflect>::type_info))
            }
        }
    };
    let input_count = sig.inputs.len();
    let mut type_id_token = quote!(None);
    let mut class_id_token = quote!(None);
    if let Some(class_token) = class_token {
        class_id_token = quote!(Some(std::any::TypeId::of::<#class_token>()));
    }
    if let Some(ty_token) = ty_token {
        type_id_token = quote!(Some(std::any::TypeId::of::<#ty_token>()));
    }

    let ty_fn_ident = match ty_token {
        Some(ty_token) => quote!(#ty_token::#ident),
        None => quote!(#ident),
    };
    if callable {
        if is_async {
            function_token = quote!(::<#function_token _>);

            let async_ty_alias_token =
                Ident::new(&format!("AsyncRetFn{input_count}"), Span::call_site());

            if trait_name.is_none() {
                function_token = quote! {
                    let ret_async_fn: glass::RetAsyncFn<_, _> = glass::#async_ty_alias_token #function_token ::new(#ty_fn_ident);
                    let function = Some(glass::FunctionWrapper::async_function(Box::new(ret_async_fn.func)));
                };
            } else {
                function_token = quote! {
                    let ret_async_fn: glass::RetAsyncFn<_, _> = glass::#async_ty_alias_token #function_token ::new(#ty_fn_ident);
                    let function = Some(glass::FunctionWrapper::trait_sync_function(Box::new(ret_async_fn.func)));
                };
            }
        } else {
            if !function_token.is_empty() {
                function_token = quote!(<#function_token>);
            }
            let ty_alias_token = Ident::new(&format!("RetFn{input_count}"), Span::call_site());
            function_token = quote! {
                let sync_fn = #ty_fn_ident as glass::#ty_alias_token #function_token;
                let function = Some(glass::FunctionWrapper::sync_function(Box::new(sync_fn)));
            };
        }
    } else {
        function_token = quote! {
            let function = None;
        }
    }

    let mut prefix = match ty_token {
        Some(ty_token) => ty_token.to_string(),
        None => String::default(),
    };
    if let Some(trait_name) = trait_name {
        prefix = format!("{}_{}", trait_name, prefix);
    }
    let mut generated_fn_name = format!("__submit_fn_{prefix}_{ident_name}").to_lowercase();
    generated_fn_name = remove_special_chars(&generated_fn_name, &['<', '>', ' ', ':']);

    let generated_fn_ident = Ident::new(&generated_fn_name, Span::call_site());
    let submit_item_token = quote! {
        fn #generated_fn_ident() -> glass::FunctionMeta {
            #function_token
            glass::FunctionMeta {
                class_id: #class_id_token,
                type_id: #type_id_token,
                trait_name: #trait_name_token,
                ident: #ident_name.into(),
                inputs: vec![#inputs_token],
                output: #output_token,
                function,
                docs: #docs_token,
                attrs: #attrs_token,
                module: module_path!().into(),
            }
        }

        #[allow(clippy::redundant_closure)]
        inventory::submit! {
            glass::FunctionSubmitItem {
                get_function_meta_fn: #generated_fn_ident,
            }
        }
    };
    Ok(submit_item_token)
}

fn remove_special_chars(input: &str, chars_to_remove: &[char]) -> String {
    input
        .chars()
        .filter(|c| !chars_to_remove.contains(c))
        .collect()
}

pub fn expand(attr: &mut ReflectAttr, item: &mut Item) -> Result<TokenStream> {
    let class_token = attr.class.as_mut().map(|t| t.to_token_stream());
    let callable = attr.callable.unwrap_or(true);
    let mut generated = match item {
        Item::Fn(item_fn) => generate_submit_item(
            &item_fn.attrs,
            &item_fn.sig,
            &class_token,
            &None,
            &None,
            callable,
        )?,
        Item::Impl(item_impl) => expand_impl(&class_token, item_impl, callable)?,
        Item::Trait(item_trait) => expand_trait(&class_token, item_trait)?,
        _ => todo!(),
    };
    generated = quote! {
        #generated
        #item
    };
    Ok(generated)
}

#[derive(SynParse)]
pub struct ReflectAttr {
    pub class: Option<Path>,
    pub callable: Option<bool>,
}

pub struct NameAttr(pub String);

impl syn::parse::Parse for NameAttr {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        // let content;
        // parenthesized!(content in input);
        let name = input.parse::<LitStr>()?.value();
        Ok(NameAttr(name))
    }
}
